﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Threading;
using System.Globalization;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using System.Security.Cryptography;
using Microsoft.Win32;

using RightEdge.Common;

using MySql.Data.MySqlClient;

namespace RightEdge_MySQL_Plugin
{
    public class FunctionResult
    {
        public FunctionResult() { }
        public FunctionResult(FunctionResult src) { _error = src._error; _message = src._message; }

        private bool _error = false;
        public bool Error { set { _error = value; } get { return (_error); } }

        private string _message = string.Empty;
        public string Message { set { _message = value; } get { return (_message); } }

        public void setError(string m) { _message = m; _error = true; }
        public void clearError() { _message = string.Empty; _error = false; }

        public static FunctionResult newError(string m) { FunctionResult fr = new FunctionResult(); fr.setError(m); return fr; }
    }

    public class FunctionObjectResult<T> : FunctionResult
    {
        public FunctionObjectResult() { }
        public FunctionObjectResult(FunctionObjectResult<T> src) : base(src) { ResultObject = src.ResultObject; }

        public T ResultObject = default(T);
    }

    public interface INamedObject
    {
        string Name { get; }
        void SetName(string n);
    }
    public class NamedConverter : TypeConverter
    {
        public override object ConvertTo(
                 ITypeDescriptorContext context,
                 CultureInfo culture,
                 object value,
                 Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                if (value is INamedObject)
                {
                    return ((INamedObject)value).Name;
                }
                else
                {
                    return "";
                }
            }

            return base.ConvertTo(
                context,
                culture,
                value,
                destinationType);
        }
    }

    public class NewFilePickUITypeEditor : UITypeEditor
    {
        NewFilePickUITypeEditor() : base() { }
        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            return UITypeEditorEditStyle.Modal;
        }
        public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
        {
            OpenFileDialog form = new OpenFileDialog();

            form.FileName = (string)value;
            form.Filter = "data files (*.xml;*.csv)|*.xml;*.csv|log files (*.log)|*.log|all files (*.*)|*.*";

            if (form.FileName.EndsWith(".xml")) { form.FilterIndex = 1; }
            else if (form.FileName.EndsWith(".csv")) { form.FilterIndex = 1; }
            else if (form.FileName.EndsWith(".log")) { form.FilterIndex = 2; }
            else { form.FilterIndex = 3; }

            form.CheckFileExists = false;
            form.CheckPathExists = true;

            DialogResult fres = form.ShowDialog();
            if (fres != DialogResult.OK) { return value; }

            if (form.FileName == (string)value) { return value; }
            else { return form.FileName; }
        }
    }

    #region xml serialization interface and base class
    public interface IXMLFileSerialize
    {
        string FileName { set; get; }

        DateTime LastFileModification { get; }
        DateTime LastFileLoad { get; }

        FunctionResult waitForUpdate(int s);
        FunctionResult waitForUpdate(DateTime prev, int s);

        FunctionResult clear();
        FunctionResult refresh<T>() where T : IXMLFileSerialize, new();
        FunctionResult refresh(Type t);

        FunctionResult saveSettings<T>() where T : IXMLFileSerialize, new();
        FunctionResult saveSettings(Type t);

        FunctionResult loadSettings<T>(string opt_fname) where T : IXMLFileSerialize, new();
        FunctionResult loadSettings(Type t, string opt_fname);

        FunctionResult loadSettings<T>(System.Xml.XmlReader reader) where T : new();
        FunctionResult loadSettings(Type t, System.Xml.XmlReader reader);

        FunctionResult initFromSerialized<T>(T src);
        FunctionResult initFromSerialized(Type t, object src);
    }

    public class XMLFileSerializeBase : IXMLFileSerialize
    {
        public XMLFileSerializeBase()
        {
            _last_modify_ts = DateTime.MinValue;
            _last_load_ts = DateTime.MinValue;
        }

        private string _file_name = string.Empty;
        [XmlIgnore, Browsable(false)]
        public string FileName { set { _file_name = value; } get { return (_file_name); } }

        private DateTime _last_modify_ts;
        [XmlIgnore, Browsable(false)]
        public DateTime LastFileModification { set { _last_modify_ts = value; } get { return (_last_modify_ts); } }
        private DateTime _last_load_ts;
        [XmlIgnore, Browsable(false)]
        public DateTime LastFileLoad { set { _last_load_ts = value; } get { return (_last_load_ts); } }

        public FunctionResult saveSettings<T>() where T : IXMLFileSerialize, new()
        {
            if (string.IsNullOrEmpty(_file_name))
            {
                return FunctionResult.newError("Unable to serialize to empty file name.");
            }
            try
            {
                XmlSerializer mySerializer = new XmlSerializer(typeof(T));
                StreamWriter myWriter = new StreamWriter(_file_name);
                mySerializer.Serialize(myWriter, this);
                myWriter.Close();
                myWriter.Dispose();
                FileInfo fi = new FileInfo(_file_name);
                _last_modify_ts = fi.LastWriteTime;
                return new FunctionResult();
            }
            catch (Exception e)
            {
                return FunctionResult.newError("saveSettings exception : " + e.Message);
            }
        }
        public FunctionResult saveSettings(Type t)
        {
            if (string.IsNullOrEmpty(_file_name))
            {
                return FunctionResult.newError("Unable to serialize to empty file name.");
            }
            try
            {
                XmlSerializer mySerializer = new XmlSerializer(t);
                StreamWriter myWriter = new StreamWriter(_file_name);
                mySerializer.Serialize(myWriter, this);
                myWriter.Close();
                myWriter.Dispose();
                FileInfo fi = new FileInfo(_file_name);
                _last_modify_ts = fi.LastWriteTime;
                return new FunctionResult();
            }
            catch (Exception e)
            {
                return FunctionResult.newError("saveSettings exception : " + e.Message);
            }
        }

        public static FunctionObjectResult<T> newFromSettings<T>(string fname) where T : IXMLFileSerialize, new()
        {
            FunctionObjectResult<T> tres = new FunctionObjectResult<T>();
            T t = new T();
            FunctionResult lres = t.loadSettings<T>(fname);
            if (lres.Error)
            {
                tres.setError("unable to de-serialize and load file '" + fname + "'. " + lres.Message);
                return tres;
            }
            tres.ResultObject = t;
            return tres;
        }
        public static FunctionObjectResult<object> newFromSettings(Type t, string fname)
        {
            FunctionObjectResult<object> tres = new FunctionObjectResult<object>();
            object o = t.Assembly.CreateInstance(t.FullName);

            FunctionResult lres = ((IXMLFileSerialize)o).loadSettings(t, fname);
            if (lres.Error)
            {
                tres.setError("unable to de-serialize and load file '" + fname + "'. " + lres.Message);
                return tres;
            }
            tres.ResultObject = o;
            return tres;
        }

        public virtual FunctionResult initFromSerialized<T>(T src)
        {
            _last_load_ts = DateTime.Now;
            return new FunctionResult();
        }
        public virtual FunctionResult initFromSerialized(Type t, object src)
        {
            _last_load_ts = DateTime.Now;
            return new FunctionResult();
        }

        public virtual FunctionResult clear()
        {
            return (new FunctionResult());
        }
        public FunctionResult refresh<T>() where T : IXMLFileSerialize, new()
        {
            FileInfo fi = new FileInfo(_file_name);

            if (fi.LastWriteTime > _last_modify_ts)
            {
                clear();//FIX ME - there should be a way to undo this if the load fails
                FunctionResult fres = loadSettings<T>(_file_name);
                if (fres.Error) { return fres; }
                _last_modify_ts = fi.LastWriteTime;
            }
            return new FunctionResult();
        }
        public FunctionResult refresh(Type t)
        {
            FileInfo fi = new FileInfo(_file_name);

            if (fi.LastWriteTime > _last_modify_ts)
            {
                clear();//FIX ME - there should be a way to undo this if the load fails
                FunctionResult fres = loadSettings(t, _file_name);
                if (fres.Error) { return fres; }
                _last_modify_ts = fi.LastWriteTime;
            }
            return new FunctionResult();
        }

        public FunctionResult loadSettings<T>(string fname) where T : IXMLFileSerialize, new()
        {
            XmlSerializer mySerializer = new XmlSerializer(typeof(T));
            FileStream myFileStream = null;
            try
            {
                myFileStream = new FileStream(fname, FileMode.Open);
            }
            catch (System.IO.IOException e)
            {
                _file_name = fname;
                return FunctionResult.newError("File Load Error : " + e.Message);
            }
            T t = (T)mySerializer.Deserialize(myFileStream);
            myFileStream.Dispose();
            _file_name = fname;
            return initFromSerialized<T>(t);
        }
        public FunctionResult loadSettings(Type t, string fname)
        {
            XmlSerializer mySerializer = new XmlSerializer(t);
            FileStream myFileStream = null;
            try
            {
                myFileStream = new FileStream(fname, FileMode.Open);
            }
            catch (System.IO.IOException e)
            {
                _file_name = fname;
                return FunctionResult.newError("File Load Error : " + e.Message);
            }
            object o = mySerializer.Deserialize(myFileStream);
            myFileStream.Dispose();
            _file_name = fname;
            return initFromSerialized(t, o);
        }

        public FunctionResult loadSettings<T>(System.Xml.XmlReader reader) where T : new()
        {
            XmlSerializer mySerializer = new XmlSerializer(typeof(T));
            object o = mySerializer.Deserialize(reader);
            return initFromSerialized<T>((T)o);
        }
        public FunctionResult loadSettings(Type t, System.Xml.XmlReader reader)
        {
            XmlSerializer mySerializer = new XmlSerializer(t);
            object o = mySerializer.Deserialize(reader);
            return initFromSerialized(t, o);
        }


        public FunctionResult waitForUpdate(int s)
        {
            FileInfo fi = new FileInfo(FileName);
            return waitForUpdate(fi.LastWriteTime, s);
        }
        public FunctionResult waitForUpdate(DateTime lts, int s)
        {
            FileInfo fi = new FileInfo(FileName);
            int sc = 0;
            do
            {
                if (fi.LastWriteTime > lts)
                { break; }
                System.Threading.Thread.Sleep(1000);
                fi.Refresh();
                sc++;
            } while (sc < s);
            if (sc >= s)
            {
                return FunctionResult.newError("No update found in time alloted.");
            }

            return new FunctionResult();
        }
    }
    #endregion

    public class PWSec
    {
        private static byte[] _entropy = { 1, 5, 89, 3, 1, 57 };
        public static string Encrypt(string s)
        {
            if (string.IsNullOrEmpty(s)) { return string.Empty; }

            byte[] src = new byte[s.Length];
            int i = 0;
            foreach (char c in s.ToCharArray()) { src[i++] = (byte)c; }

            byte[] r = ProtectedData.Protect(src, _entropy, DataProtectionScope.CurrentUser);
            char[] res = new char[r.Length * 4];
            i = 0;
            foreach (byte b in r) { foreach (char bc in b.ToString("X4").ToCharArray()) { res[i++] = bc; } }

            return new string(res);
        }
        public static string Decrypt(string s)
        {
            if (string.IsNullOrEmpty(s)) { return string.Empty; }

            byte[] src = new byte[s.Length / 4];
            int i = 0;
            int bi = 0;
            char[] bstr = new char[4];
            foreach (char c in s.ToCharArray())
            {
                if (bi < 4) { bstr[bi++] = c; continue; }
                else
                {//decode "X4" encoded byte string 'bstr' into src[i++]
                    src[i++] = byte.Parse(new string(bstr), System.Globalization.NumberStyles.AllowHexSpecifier);
                    bi = 0;
                    bstr[bi++] = c;
                }
            }
            if (bi != 4) { throw new ArgumentException("encrypted string not in X4 byte encoding"); }
            src[i] = byte.Parse(new string(bstr), System.Globalization.NumberStyles.AllowHexSpecifier);//copy the final buffer
            byte[] r = ProtectedData.Unprotect(src, _entropy, DataProtectionScope.CurrentUser);
            char[] res = new char[r.Length];
            i = 0;
            foreach (byte b in r) { res[i++] = (char)b; }
            return new string(res);
        }
    }

    public interface IPluginLogOptions
    {
        string FileName { set; get; }

        bool LogExceptions { set; get; }
        bool LogErrors { set; get; }
        bool LogDebug { set; get; }
        bool ShowErrors { set; get; }
    }

    public class PluginLogOptionsEditor : UITypeEditor
    {
        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            return UITypeEditorEditStyle.Modal;
        }
        public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
        {
            if (provider == null) { return value; }
            IWindowsFormsEditorService edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
            if (edSvc == null) { return value; }

            PluginLogOptionsForm form = new PluginLogOptionsForm(new PluginLogOptions((PluginLogOptions)value));
            DialogResult fres = edSvc.ShowDialog(form);
            if (fres != DialogResult.OK) { return value; }

            PluginLogOptions vpl = (PluginLogOptions)value;
            PluginLogOptions editpl = form.LogOptions;
            //compare editpl and form.log, if no changes return value
            if (vpl.LogDebug == editpl.LogDebug &&
                vpl.LogErrors == editpl.LogErrors &&
                vpl.LogExceptions == editpl.LogExceptions &&
                vpl.ShowErrors == editpl.ShowErrors &&
                vpl.FileName == editpl.FileName)
            { return value; }
            else// value changed
            { return editpl; }
        }
    }

    [Serializable]
    [Editor(typeof(PluginLogOptionsEditor), typeof(UITypeEditor))]
    [TypeConverter(typeof(NamedConverter))]
    public class PluginLogOptions : IPluginLogOptions,INamedObject
    {
        public PluginLogOptions() { initDefaultFromRegistry(); }
        public PluginLogOptions(string fn) { _log_fname = fn; }
        public PluginLogOptions(PluginLogOptions src) { Copy(src); }

        private void initDefaultFromRegistry()
        {//get the directories from the registry...
            string log_dir = "C:\\";
            try
            {
                log_dir = (string)Registry.GetValue("HKEY_CURRENT_USER\\Software\\RightEdgeMySQLPlugin", "LogDir", "C:\\");
            }
            catch (Exception)
            {
            }
            _log_fname = log_dir + "log.log";
        }

        public void Copy(PluginLogOptions src)
        {
            PluginLogOptions rsrc = src;
            if (src == null) { rsrc = new PluginLogOptions(); }

            _log_debug = rsrc._log_debug;
            _log_errors = rsrc._log_errors;
            _log_exceptions = rsrc._log_exceptions;
            _log_fname = rsrc._log_fname;
            _show_errors = rsrc._show_errors;
        }
        private string _log_fname = "C:\\log.log";//FIX ME - this should be set from the installed app log dir
        [Description("Set this to the desired log file name."), Editor(typeof(NewFilePickUITypeEditor), typeof(UITypeEditor))]
        public string FileName { set { _log_fname = value; } get { return (_log_fname); } }
        public string Name { get { return (_log_fname); } }
        public void SetName(string n) { _log_fname = n; }

        private bool _log_errors = true;
        [Description("Set this to true to enable logging of errors.")]
        public bool LogErrors { set { _log_errors = value; } get { return (_log_errors); } }

        private bool _show_errors = false;
        [Description("Set this to true to enable a message box dialog on errors.")]
        public bool ShowErrors { set { _show_errors = value; } get { return (_show_errors); } }

        private bool _log_exceptions = true;
        [Description("Set this to true to enable logging of exception details.")]
        public bool LogExceptions { set { _log_exceptions = value; } get { return (_log_exceptions); } }

        private bool _log_debug = true;
        [Description("Set this to true to enable debug messages.")]
        public bool LogDebug { set { _log_debug = value; } get { return (_log_debug); } }
    }

    public class PluginLog : IPluginLogOptions
    {
        public PluginLog() { }
        public PluginLog(IPluginLogOptions src) { Copy(src); }

        ~PluginLog() { closeLog(); }

        public void Copy(IPluginLogOptions src)
        {
            _log_debug = src.LogDebug;
            _log_errors = src.LogErrors;
            _log_exceptions = src.LogExceptions;
            _fname = src.FileName;
            _show_errors = src.ShowErrors;
        }

        private bool _log_exceptions = true;
        public bool LogExceptions { set { _log_exceptions = value; } get { return (_log_exceptions); } }

        private bool _log_errors = true;
        public bool LogErrors { set { _log_errors = value; } get { return (_log_errors); } }

        private bool _log_debug = true;
        public bool LogDebug { set { _log_debug = value; } get { return (_log_debug); } }

        private bool _show_errors = true;
        public bool ShowErrors { set { _show_errors = value; } get { return (_show_errors); } }

        public bool IsOpen { get { return (_fs != null); } }

        private string _fname = null;
        public string FileName { get { return (_fname); } set { closeLog(); _fname = value; } }

        private FileStream _fs = null;
        private static object _lock = new object();

        private int _monitor_timeout = 1000;

        public void closeLog()
        {
            if (!Monitor.TryEnter(_lock, _monitor_timeout))
            {
                throw new Exception("Unable to acquire lock on log file stream.");
            }
            try
            {
                if (_fs == null) { return; }

                _fs.Close();
                _fs = null;
            }
            finally { Monitor.Pulse(_lock); Monitor.Exit(_lock); }
        }
        public void openLog()
        { openLog(true); }

        private void openLog(bool do_lock)
        {
            if (do_lock && !Monitor.TryEnter(_lock, _monitor_timeout))
            {
                throw new Exception("Unable to acquire lock on log file stream.");
            }
            try
            {
                if (_fs != null) { return; }
                _fs = new FileStream(_fname, FileMode.Append, FileAccess.Write,FileShare.ReadWrite);
            }
            catch (Exception e)
            {
                throw new Exception("log open error : " + e.Message, e);
            }
            finally { if (do_lock) { Monitor.Pulse(_lock); Monitor.Exit(_lock); } }
        }
        protected void writeMessage(string message)
        {
            if (!Monitor.TryEnter(_lock, _monitor_timeout))
            {
                throw new Exception("Unable to acquire lock on log file stream.");
            }

            try
            {
                if (!IsOpen) { openLog(false); }
                string msg = DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] : " + message + "\n";
                byte[] msg_bytes = new UTF8Encoding(true).GetBytes(msg);

                _fs.Write(msg_bytes, 0, msg_bytes.Length);
                _fs.Flush();
            }
            catch (Exception e)
            {
                throw new Exception("log write error : " + e.Message, e);
            }
            finally { Monitor.Pulse(_lock); Monitor.Exit(_lock); }
        }

        public void captureException(Exception e)
        {
            if (!_log_exceptions) { return; }
            string m = "";
            bool is_inner = false;
            for (Exception oe = e; oe != null; oe = oe.InnerException)
            {
                if (is_inner)
                { m += "\tInnerException:\n" + "\t" + oe.TargetSite + "\n\t" + oe.Message + "\n" + oe.StackTrace + "\n"; }
                else
                { m += "Exception:\n" + oe.TargetSite + "\n" + oe.Message + "\n" + oe.StackTrace + "\n"; }
                if (!is_inner) { is_inner = true; }
            }
            writeMessage(m);
        }
        public void captureDebug(string m)
        {
            if (!_log_debug) { return; }
            writeMessage(m);
        }

        public void captureError(string message, string title)
        {
            if (_log_errors) { writeMessage("ERROR : " + message); }
            if (_show_errors)
            {
                MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}